﻿#region Copyright & licence

// This file is part of Refix.
// 
// Refix is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
// 
// Refix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with Refix.  If not, see <http://www.gnu.org/licenses/>.
// 
// Copyright (C) 2010-11 David Musgrove and others.

#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Xml.XPath;

using Refix.Core.Repository;

namespace Refix.Core.IO
{
    [Serializable]
    public class ProjectFile
    {
        private const string MSBUILD_SCHEMA = "http://schemas.microsoft.com/developer/msbuild/2003";
        static private readonly XmlNamespaceManager _namespaceManager;

        private readonly XDocument _projectFile;
        private readonly string _relativePath;
        private readonly SolutionFile _solutionFile;

        static ProjectFile()
        {
            _namespaceManager = GetNamespaceManager();
        }

        private ProjectFile()
        {
            ProjectReferences = new List<ProjectReference>();
            AssemblyBindingRedirects = new List<Reference>();
            References = new List<Reference>();
        }

        public ProjectFile(SolutionFile solutionFile, string fileName) : this()
        {
            FileName = fileName;

            _solutionFile = solutionFile;
            _projectFile = XDocument.Load(fileName);

            _relativePath = PathUtilities.MakeRelativePath(
                Path.GetDirectoryName(fileName) + "\\",
                Path.Combine(Path.GetDirectoryName(solutionFile.FileName), ".refix") + "\\");

            XElement guidElement = _projectFile.XPathSelectElement("/ms:Project/ms:PropertyGroup/ms:ProjectGuid",
                                                                   _namespaceManager);
            if (guidElement != null)
            {
                ProjectGuid = new Guid(guidElement.Value);
                if (!solutionFile.CachedProjectFiles.ContainsKey(ProjectGuid))
                {
                    solutionFile.CachedProjectFiles.Add(ProjectGuid, this);
                }
            }

            AddReferences();
            AddProjectReferences();
        }

        public bool IsWebProject()
        {
            XElement webProjectElement = _projectFile.XPathSelectElement(
                "//ms:WebProjectProperties", _namespaceManager);
            return webProjectElement != null;
        }

        public decimal GetTargetFrameworkVersion()
        {
            XElement versionElement = _projectFile.XPathSelectElement(
                "/ms:Project/ms:PropertyGroup/ms:TargetFrameworkVersion", _namespaceManager);
            if (versionElement == null)
            {
                return decimal.MaxValue;
            }
            return decimal.Parse(versionElement.Value.Substring(1));
        }

        public string GetBuildOutputFolder(string configuration)
        {
            XElement pathElement = _projectFile.XPathSelectElement(
                "/ms:Project/ms:PropertyGroup[contains(@Condition,\"'$(Configuration)|$(Platform)' == '" + configuration +
                "|AnyCPU'\")]/ms:OutputPath", _namespaceManager);
            if (pathElement != null)
            {
                return pathElement.Value;
            }
            return null;
        }

        public string FileName { get; set; }

        [XmlElement]
        public List<Reference> References { get; set; }

        [XmlElement]
        public List<Reference> AssemblyBindingRedirects { get; set; }

        [XmlElement]
        public List<ProjectReference> ProjectReferences { get; set; }

        [XmlAttribute]
        public Guid ProjectGuid { get; set; }

        private void EnsureConfigurationFileIncluded()
        {
            // Web project will have a Web.config, so we can just check
            // and return if found.
            if (IsWebProject())
            {
                return;
            }
            XElement noneElement = _projectFile.XPathSelectElement("/ms:Project/ms:ItemGroup[ms:None]",
                                                                   _namespaceManager);
            if (noneElement == null)
            {
                XElement compileElement = _projectFile.XPathSelectElement("/ms:Project/ms:ItemGroup[ms:Compile]",
                                                                          _namespaceManager);
                noneElement = new XElement(XName.Get("ItemGroup", MSBUILD_SCHEMA));
                compileElement.AddAfterSelf(noneElement);
            }

            XElement configElement = noneElement.XPathSelectElement("ms:None[@Include='App.config']",
                                                                    _namespaceManager);
            if (configElement == null)
            {
                configElement = new XElement(XName.Get("None", MSBUILD_SCHEMA));
                configElement.SetAttributeValue(XName.Get("Include"), "App.config");
                noneElement.Add(configElement);
            }
        }

        private void AddProjectReferences()
        {
            foreach (
                XElement element in
                    _projectFile.XPathSelectElements("/ms:Project/ms:ItemGroup/ms:ProjectReference",
                                                     _namespaceManager))
            {
                XElement nameElement = element.Element(XName.Get("Name", MSBUILD_SCHEMA));
                XElement projectElement = element.Element(XName.Get("Project", MSBUILD_SCHEMA));
                string projectRelativePath = element.Attribute("Include").Value;
                var projectReference = new ProjectReference
                                           {
                                               Name = nameElement == null ? null : nameElement.Value,
                                               Path = projectRelativePath
                                           };
                if (projectElement != null)
                {
                    projectReference.Project = new Guid(projectElement.Value);
                }
                ProjectReferences.Add(projectReference);
                if (projectReference.Project != Guid.Empty)
                {
                    ProjectFile projectFile;
                    if (_solutionFile.CachedProjectFiles.TryGetValue(projectReference.Project, out projectFile))
                    {
                        projectReference.ProjectFile = projectFile;
                        continue;
                    }
                    string projectPath = Path.Combine(Path.GetDirectoryName(FileName), projectRelativePath);
                    projectPath = new DirectoryInfo(projectPath).FullName;

                    projectReference.ProjectFile = new ProjectFile(_solutionFile, projectPath);
                }
            }
        }

        private void AddReferences()
        {
            foreach (
                XElement element in
                    _projectFile.XPathSelectElements("/ms:Project/ms:ItemGroup/ms:Reference", _namespaceManager))
            {
                string assemblyName = element.Attribute("Include").Value;
                if (assemblyName.IsSystemAssemblyName())
                {
                    continue;
                }
                XElement hintPathElement = element.Element(XName.Get("HintPath", MSBUILD_SCHEMA));
                XElement specificVersionElement = element.Element(XName.Get("SpecificVersion", MSBUILD_SCHEMA));
                var reference = new Reference
                                    {
                                        FullAssemblyName = assemblyName,
                                        Node = element
                                    };
                if (hintPathElement != null)
                {
                    reference.HintPath = hintPathElement.Value;
                }
                if (specificVersionElement != null)
                {
                    reference.SpecificVersion = bool.Parse(specificVersionElement.Value);
                }
                References.Add(reference);
            }
        }

        static private XmlNamespaceManager GetNamespaceManager()
        {
            var namespaceManager = new XmlNamespaceManager(new NameTable());
            namespaceManager.AddNamespace("ms", MSBUILD_SCHEMA);
            return namespaceManager;
        }

        public List<Reference> GetAllAssemblyBindingRedirects(ProjectFile projectFile)
        {
            var all = new List<Reference>();
            all.AddRange(AssemblyBindingRedirects);
            AddChildProjects(projectFile, all, ProjectReferences);
            return all;
        }

        static private void AddChildProjects(ProjectFile projectFile, List<Reference> all,
                                             IEnumerable<ProjectReference> projectReferences)
        {
            foreach (ProjectReference projectReference in projectReferences)
            {
                ProjectReference reference = projectReference;
                ProjectFile childProjectFile =
                    projectFile._solutionFile.CachedProjectFiles.Values.Single(
                        p => Path.GetFileNameWithoutExtension(p.FileName) == Path.GetFileNameWithoutExtension(reference.Path))
                        .GetDependencyFile();
                foreach (Reference assemblyBindingRedirect in childProjectFile.AssemblyBindingRedirects)
                {
                    Reference redirect = assemblyBindingRedirect;
                    if (!all.Any(a => a.AssemblyName == redirect.AssemblyName
                                      && a.SpecifiedVersion == redirect.SpecifiedVersion))
                    {
                        all.Add(assemblyBindingRedirect);
                    }
                }
                AddChildProjects(projectFile, all, childProjectFile.ProjectReferences);
            }
        }

        public int InjectBackupAndSave(bool ensureConfigurationFile)
        {
            if (References.Count() == 0)
            {
                return 0;
            }
            if (!References.Any(r => r.SpecifiedVersion != r.ResolvedVersion
                || r.HintPath !=string.Format("{0}{1}.dll", _relativePath, r.AssemblyName)))
            {
                return 0;
            }
            File.Copy(FileName, FileName + FileExtensions.BackupFile);
            foreach (Reference reference in References)
            {
                reference.Node.SetAttributeValue("Include", reference.ResolvedFullAssemblyName);
                reference.Node.SetElementValue(XName.Get("HintPath", MSBUILD_SCHEMA),
                                               string.Format("{0}{1}.dll", _relativePath, reference.AssemblyName));
                reference.Node.SetElementValue(XName.Get("Private", MSBUILD_SCHEMA), "True");
            }
            if (ensureConfigurationFile)
            {
                EnsureConfigurationFileIncluded();
            }
            _projectFile.Save(FileName);
            return 1;
        }

        public void WriteDependencyFile()
        {
            using (StreamWriter file = File.CreateText(FileName + FileExtensions.DependencyFile))
            {
                new XmlSerializer(typeof(ProjectFile)).Serialize(file, this);
                file.Flush();
            }
        }
    }
}